﻿using System;
using System.Collections.Concurrent;
using System.Threading;

namespace AsyncLoggers
{
    /// <summary>
    /// Note: not thread-safe
    /// </summary>
    internal class CommandProcessor : IDisposable
    {
        private readonly Command _disposeCommand;
        private readonly ConcurrentQueue<Command> _commandQueue = new ConcurrentQueue<Command>();

        private bool _isDisposed;
        private readonly ManualResetEventSlim _allCommandsProcessed = new ManualResetEventSlim(false);
        private readonly ManualResetEventSlim _disposed = new ManualResetEventSlim(false);

        private readonly object _processingThreadStateChangeCriticalSection = new object();
        private bool _gotProcessingThread;
        private static readonly TimeSpan ProcessingThreadBackoffQuantum = TimeSpan.FromMilliseconds(50);

        public CommandProcessor()
        {
            _disposeCommand = new ActionCommand(
                () =>
                    {
                        _isDisposed = true;
                        _disposed.Set();
                    });
        }

        public void ProcessQueuedCommands()
        {
            ThrowIfDisposed();

            _allCommandsProcessed.Reset();

            QueueCommand(new ActionCommand(() => _allCommandsProcessed.Set()));

            _allCommandsProcessed.Wait();
        }

        public void Dispose()
        {
            if (_isDisposed)
            {
                return;
            }

            ProcessQueuedCommands();

            QueueCommand(_disposeCommand);

            _disposed.Wait();
        }

        public void QueueCommand(Command commandToQueue)
        {
            ThrowIfDisposed();

            if (commandToQueue == null) throw new ArgumentNullException("commandToQueue");

            _commandQueue.Enqueue(commandToQueue);

            CreateQueueProcessor();
        }

        private void CreateQueueProcessor()
        {
            lock (_processingThreadStateChangeCriticalSection)
            {
                if (!_gotProcessingThread)
                {
                    _gotProcessingThread = true;

                    // Note: this work can potentially be moved to the thread pool if we
                    // want some kind of waiting to explicitly occur before the process quits
                    // (it kind of does most of the time anyway)
                    var loggingThread = new Thread(ProcessQueue) { IsBackground = false };
                    loggingThread.Start();
                }
            }
        }

        private void ProcessQueue()
        {
            while (!_isDisposed)
            {
                Command command;

                int successiveIdleTimeSlots = 0;

                while (successiveIdleTimeSlots < 3)
                {
                    if (!_commandQueue.TryDequeue(out command))
                    {
                        successiveIdleTimeSlots++;

                        Thread.Sleep(ProcessingThreadBackoffQuantum);
                        continue;
                    }
                    else
                    {
                        command.Execute();
                        successiveIdleTimeSlots = 0;
                    }
                }

                lock (_processingThreadStateChangeCriticalSection)
                {
                    if (_commandQueue.Count > 0)
                    {
                        continue;
                    }

                    _gotProcessingThread = false;
                    return;
                }
            }
        }

        private void ThrowIfDisposed()
        {
            if (_isDisposed)
            {
                throw new ObjectDisposedException(GetType().FullName);
            }
        }
    }
}